package Server;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.sun.net.httpserver.HttpExchange;

import Common.Base64;
import Common.Constants;
import Common.Utils;

public class ServerUtils {
	
	//debug flag
	static boolean debugFlag = true;
	
	public static final String DEFAULT_DIR = "Storage";

	public static final String LISTING_FILE_NAME = "listing.dat";

	public static final String DIR_SEPERATOR = File.separator;

	public static Map<String, String> getQueryMap(String query)  
	{  
		String[] params = query.split("&");  
		Map<String, String> map = new HashMap<String, String>();  
		for (String param : params)  
		{  
			String name = param.split("=")[0];  
			String value = param.split("=")[1];  
			map.put(name, value);  
		}  
		return map;  
	} 

	/* a function  to convert from client-side file name-space to server-side
	 * 
	 */
	public static String convertToLocalFilename(byte[] hash) throws IOException{
		String hashBase64 = Base64.encodeBytes(hash, Base64.URL_SAFE);
		return hashBase64;
	} 

	

	

	

	

	//overloaded method
	//	public static ListingEntry getEntry(String filename, String owner, int snapshot){
	//		
	//		for (ListingEntry entry : AlibabaServer.fileList.values()){
	//			if (entry.getFilename().equals(filename)
	//					&& entry.getOwner().equals(owner)
	//					&& Utils.md256equal(signature, entry.getFileHash())
	//					){
	//				return entry;
	//			}
	//		}
	//		
	//		return null;
	//	}

	/* consistCheck checks if we can work with the directory
	 * and for every file: 1) is the file on list but not on disk - remove from list
	 * 					   2) is the file's hash on disk different from what is on the list - replace with the one on the disk  
	 * 
	 * @param Listing listing is the listing of files to check
	 * @param String dir is the directory to check against the listing given
	 * 
	 * @return boolean true iff the dir is consistent with the listing
	 * 
	 * @throw
	 */
	public static boolean consistCheck(Listing listing, RefList refList, String dir){

		boolean retFlag = true;
		// check the directory
		File directory = new File(dir);

		try {
			if(!Utils.isDirOK(dir,directory)){
				return false;//because the dir is problematic
			}
		} catch (IOException e) {
			System.out.println("There was a problem with the directory " + dir);
			e.printStackTrace();
			return false;
		}

		Map<String, ListingEntry> fileList = listing.getListOfFiles();
		List<String> blackList = new ArrayList<String>();

		// check if files on the list exist on the disk
		for (String key : fileList.keySet()) {
			ListingEntry entry = fileList.get(key);
			String filename = entry.getFilename();
			String localFilename = entry.getLocalName();

			if (!refList.hasEntry(localFilename)){	// no entry yet for this file
				refList.addEntry(localFilename);
				refList.addCount(localFilename, entry.getSnapshotVersion().length - 1);
			}
			else {		// there is an entry for this file add ref-count for each snapshot it is in
				refList.addCount(localFilename, entry.getSnapshotVersion().length);
			}

			File file = new File(dir + localFilename);
			if (!file.exists()){
				retFlag = false;		//return false on consistency
				blackList.add(key);	//the map throws an exception if we remove an entry on the fly while iterating
				System.out.println("Error: missing file: " + filename + " on disk, removed the entry from the listing.");
			}
			else {
				byte[] fileBytes = null;
				try {
					fileBytes = Utils.getBytesFromFile(file);

				} catch (IOException e) {
					e.printStackTrace();
					System.out.println("Error occured, consistency check did not complete.");
					return false;
				}
				byte[] fileHash = Utils.getSHA256hash(fileBytes);
				if (!Utils.md256equal(fileHash, entry.getFileHash())){
					System.out.println("The hash of file: " + filename + " is inconsistent with the listing!");
					System.out.println("replaced the hash of file on the listing to hash of file on disk.");
					retFlag = false;	//return false on consistency
					entry.setFileHash(fileHash);
				}
			}
		}	// end of for loop

		//snapshots consistency check - there must not be a snapshotID that points to 2 distinguished
		//versions of a file in the file list.
		//we go over 




		if (!blackList.isEmpty()){
			for (String key : blackList){
				fileList.remove(key);
			}
		}

		return retFlag;
	}

	/* a simple predicate to verify if the String path is a file in listing
	 * DEPRECATED
	 */
	public static boolean isFile(Listing listing, String path){	
		if (listing.getListOfFiles().get(path) != null) return true;
		return false;
	}

	/* a predicate to verify if the path String is for a dir.
	 * (according to ex. specs.
	 */
	public static boolean isDir(String path){
		if (path.endsWith("/")) return true;
		return false;
	}


	/* this method creates a list of files in the given directory name according to the spec.
	 * and returns a list of the file names
	 */
	public static List<ListingEntry> lsDir(String dirName, Listing listing, String owner, int snapshot){
		List<ListingEntry> list = new ArrayList<ListingEntry>();

		if (!dirName.endsWith(Utils.URL_SEPARATOR)){
			dirName = dirName.concat(Utils.URL_SEPARATOR);
		}

		for (ListingEntry entry : listing.getListOfFiles().values()){
			if (entry.getOwner().equals(owner)
					&& ( entry.isInSnapshot(snapshot) || (snapshot == Constants.UNSPEC_SNAPSHOT ) )	// one of the conditions must be met - used for check operations
					&& entry.getFilename().startsWith(dirName) 
					&& (entry.getFilename().indexOf(Utils.URL_SEPARATOR, dirName.length())) == -1){
				list.add(entry);
			}
		}
		if (list.size() == 0) return null;
		return list;
	}

	/* creates string of the file names in the given list and returns it's byte[]
	 * the string is in the format: <filename1)>\n<filename2>\n...<filename(n)>\n
	 * if the list is empty this method returns null
	 */
	public static byte[] createLSResponse(List<ListingEntry> fileList){
		if (fileList.isEmpty()) return "".getBytes();

		StringBuffer list = new StringBuffer();
		for (ListingEntry entry : fileList){
			list = list.append(entry.getFilename() + "\n");
		}

		return list.toString().getBytes();
	}


	





	public static void sendResponse(HttpExchange exchange, byte[] body) throws IOException{
		exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
		OutputStream responseBody = exchange.getResponseBody();
		responseBody.write(body);
		responseBody.close();
		return;		
	}

	public static void deleteFile(String prevLocalFilename) {
		File file = new File(prevLocalFilename);
		if (file.exists()){
			file.delete();		
			return;
		}

	}

	// add the current snapshot mark to the entry
	public static void addSnapshotPtrToEntry(ListingEntry entry, int snapshotID){
		int[] oldArray = entry.getSnapshotVersion();
		int[] newArray = null;

		newArray = new int[oldArray.length+1];

		newArray[newArray.length-1] = snapshotID;
		for (int i = 0; i < oldArray.length; i++){
			newArray[i] = oldArray[i];
		}

		entry.setSnapshotVersion(newArray);
		return;
	}


	/* remove the current snapshot mark from the entry
	 */
	public static void removeSnapshotPtrFromEntry(ListingEntry entry, int snapshotID){
		int[] oldArray = entry.getSnapshotVersion();
		//on last snapshot mark - remove the array
		if (oldArray.length == 1){
			entry.setSnapshotVersion(null);
			return;
		}

		int[] newArray = new int[oldArray.length-1];
		for (int i = 0, j = 0; i < oldArray.length; i++, j++){
			if (oldArray[i] == snapshotID) {
				j--;
				continue;
			}
			newArray[j] = oldArray[i];
		}
		entry.setSnapshotVersion(newArray);
		return;
	}

	public static void removeFromCurrentSnapshot(ListingEntry entry) {
		removeSnapshotPtrFromEntry(entry, Constants.CURRENT_SNAPSHOT);		
	}

	public static void addToCurrentSnapshot(ListingEntry entry) {
		addSnapshotPtrToEntry(entry, Constants.CURRENT_SNAPSHOT);		
	}
	
	/*-----	Class Mehtods ---------*/

	


	// parses the snapshot number from the query string and returns it as an integer
	public static int parseSnapshot(String query){
		if (query == null) {
			return 0;
		}
		else {
			String snapshotStr = query.substring(Constants.SNAPSHOT.length()+1);
			try {
				return Integer.parseInt(snapshotStr);
			}
			catch (NumberFormatException e){
				return 0;	//if the query is string is passed incorrectly go to default: current snapshot
			}
		}
	}

	// template method: sends the response body over the connection held by exchange
	public static boolean sendResponseBody(HttpExchange exchange, byte[] payload, String errorMsg){
		try {
			ServerUtils.sendResponse(exchange, payload);
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println(errorMsg);
			sendInternErrMsg(exchange);
			return false;
		}
		return true;
	}

	

	// send back the signature requested
	public static boolean sendSignatureResponse(HttpExchange exchange, byte[] fileHash){
		String hashBase64 = null;
		try {
			hashBase64 = Base64.encodeBytes(fileHash, Base64.URL_SAFE) + "\n";
			return sendResponseBody(exchange, hashBase64.getBytes(), "IO Error.");
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("ERROR: failed to convert the hash to Base64.");
			sendInternErrMsg(exchange);
			return false;
		}
	}

	// send back the file contents int the response
	public static boolean sendFileResponse(HttpExchange exchange, String filename, String BULib){
		byte[] fileToSend;
		try {
			fileToSend = Utils.getBytesFromFile(BULib + filename);
			return sendResponseBody(exchange, fileToSend, "IO Error.");
		} catch (IOException e) {
			e.printStackTrace();
			System.out.println("ERROR: COULDN'T READ FROM FILE: " + filename);
			return false;
		}
	}

	// send back the ls reponse body
	public static boolean sendLSResponse(HttpExchange exchange, List<ListingEntry> ls){
		byte[] responseBody = ServerUtils.createLSResponse(ls);
		return sendResponseBody(exchange, responseBody, "IO Error.");
	}

	/* update the listing file fileName 
	 * return false iff an error occurred
	 */
	

	// messages template method
	public static boolean sendHeaders(HttpExchange exchange, int msgConst, String msg){
		try {
			exchange.sendResponseHeaders(msgConst, 0);
		} catch (IOException e1) {
			e1.printStackTrace();
			System.out.println(msg);

			return false;
		}
		return true;
	}

	// send internal error method through exchange
	public static boolean sendOKMsg(HttpExchange exchange){
		return sendHeaders(exchange, HttpURLConnection.HTTP_OK, "IO Error. failed to send 200 - OK message");
	}	

	// send internal error method through exchange
	public static boolean sendInternErrMsg(HttpExchange exchange){
		return sendHeaders(exchange, HttpURLConnection.HTTP_INTERNAL_ERROR, "Failed to send 500 - internal error message.");
	}

	// send file not found method through exchange
	public static boolean sendFileNotFoundMsg(HttpExchange exchange){
		return sendHeaders(exchange, HttpURLConnection.HTTP_NOT_FOUND, "Failed to send 404 - File Not Found to client.");
	}

	// send bad request msg method through exchange
	public static boolean sendBadRequestMsg(HttpExchange exchange){
		return sendHeaders(exchange, HttpURLConnection.HTTP_BAD_REQUEST, "Faile to send 400 - Bad Request to client");
	}	
	
	/* a debugging tool - prints out msg if we set debugFlag to true
	 * 
	 */
	public static void debugMsg(String msg){
		if (debugFlag){
			System.out.println("DEBUG: " + msg);
		}
		return;
	}
}
